#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Datetime: 2020/7/28 14:17
# @Author  : CHEN Wang
# @Site    :
# @File    : performance.py
# @Software: PyCharm

"""
脚本说明： 回测组合的业绩指标统计， 需要与回测模块打通，输入相关信息，直接生成绩效分析报告
"""

import os
import pandas as pd
import numpy as np
import math
import statsmodels.api as sm
from itertools import groupby
from matplotlib import pyplot as plt, dates as mdates, cm, gridspec as gridspec
from matplotlib.ticker import FuncFormatter
from quant_researcher.quant.project_tool.localize import BASE_DIR
from quant_researcher.quant.project_tool.logger.my_logger import LOG
from quant_researcher.quant.project_tool import time_tool
from quant_researcher.quant.datasource_fetch.fund_api import fund_nav_related, fund_info
from quant_researcher.quant.datasource_fetch.index_api import index_price_related
from quant_researcher.quant.project_tool.common_var import RF_RATE_DAY
from quant_researcher.quant.project_tool.df_tool import calendar_reindex

FREQ_DAYS_DICT = {
    'hourly': 1 / 24,
    'daily': 1,
    'weekly': 7,
    'monthly': 30,
    'quarterly': 90,
    'yearly': 365,
}


def annualization_factor(freq, days_365=False):
    """
    根据数据频率得到年化因子

    :param str freq: 数据频率，可选daily,weekly,monthly,quarterly,yearly
    :return: int

    """
    try:
        if not days_365:
            ANNUALIZATION_FACTORS = {
                'hourly': 252 * 24,
                'daily': 252,
                'weekly': 52,
                'monthly': 12,
                'quarterly': 4,
                'yearly': 1,
            }
            factor = ANNUALIZATION_FACTORS[freq]
        else:
            ANNUALIZATION_FACTORS = {
                'hourly': 365 * 24,
                'daily': 365,
                'weekly': 52,
                'monthly': 12,
                'quarterly': 4,
                'yearly': 1,
            }
            factor = ANNUALIZATION_FACTORS[freq]
    except KeyError:
        raise ValueError(
            "Period cannot be '{}'. "
            "Can be '{}'.".format(
                freq, "', '".join(ANNUALIZATION_FACTORS.keys())
            )
        )
    return factor


def prices_cleaning(prices):
    """
    价格序列清洗，中间为0的部分填充为Nan;  中间为Nan的部分用前值填充

    :param prices:
    :return:
    """
    prices.replace(0, np.nan, inplace=True)  # 有些价格序列价格为0， 其实是因为获取不到，应该设置为Nan
    # prices[prices < 0.000000000001] = np.nan   # 价格按道理不应该这么小，直接设置为Nan, 认为是数据有误； 不过也不一定是这样的，有些价格就是这么小的数值
    try:
        temp_prices = prices[prices[~prices.isnull()].index[0]:prices[~prices.isnull()].index[-1]]  # 前后为NAN的都剔除，中间的不剔除
    except Exception as err:
        if 'index 0 is out of bounds' in str(err):
            print(f'{prices.name} prices序列全为Nan, 请检查价格序列')
        return prices

    temp_prices = temp_prices.ffill()

    prices[temp_prices.index] = temp_prices

    return prices


def prices_to_returns(prices, log_returns=False, freq='D'):
    """
    将指数的价格序列或者基金的净值序列转换成收益率序列

    :param  pd.DataFrame or pd.Series prices: index为time，columns为一个或多个净值序列
    :param bool log_returns: whether to compute using log returns, defaults to False
    :return: array-like
    """
    if prices.shape[0] <= 1:
        LOG.error("输入的价格序列不足两行，无法转换成收益率序列，请重新输入")
        return

    prices.replace(0, np.nan, inplace=True)  # 有些价格序列价格为0， 其实是因为获取不到，应该设置为Nan
    # prices[prices < 0.000000000001] = np.nan   # 价格按道理不应该这么小，直接设置为Nan, 认为是数据有误； 不过也不一定是这样的，有些价格就是这么小的数值

    try:
        temp_prices = prices[prices[~prices.isnull()].index[0]:prices[~prices.isnull()].index[-1]]  # 前后为NAN的都剔除，中间的不剔除
    except Exception as err:
        if 'index 0 is out of bounds' in str(err):
            print(f'{prices.name} prices序列全为Nan, 请检查价格序列')
        return prices

    # result = temp_prices.ffill().bfill().pct_change().iloc[1:]
    if freq == 'D':
        freq_gap = 1
    elif freq == 'W':
        freq_gap = 7
    elif freq == 'M':
        freq_gap = 30
    returns_result = temp_prices.ffill().pct_change(periods=freq_gap)

    if log_returns:
        returns_result = returns_result + 1
        returns_result = np.log(returns_result)

    prices[returns_result.index] = returns_result

    return prices


def daily_2_monthly_return(returns):
    """
    日频收益率序列转换为月频收益率序列

    :param pd.DataFrame returns: (daily) percentage returns of the assets, each row is a
                   date and each column is a ticker/id.
    :return:
    """
    month = pd.period_range(start=returns.index[-1], end=returns.index[0], freq='M')
    monthstr = [str(month[i]) for i in range(len(month))]
    monthreturn = pd.DataFrame(columns=returns.columns)
    for i in range(len(monthstr)):
        a = pd.DataFrame((returns[monthstr[i]] + 1).prod(axis=0) - 1, columns=[monthstr[i]])
        monthreturn = monthreturn.append(a.T)
    return monthreturn


def aggregate_returns(returns, convert_to):
    """
    将日频收益率序列转换成周频、月频及年频
    todo 得剔除不完整的周，月，年； 上面的函数daily_2_monthly_return是这个功能的一个特例，需要整合进来

    :param pd.Series returns: 日频收益率序列，index为datetime.date格式
    :param str convert_to: weekly-周频，monthly-月频，quarterly-季频，yearly-年频
    :return: pd.Series
    """

    def cumulate_returns(x):
        return np.exp(np.log(1 + x).cumsum())[-1] - 1

    if convert_to == 'weekly':
        return returns.groupby(
            [lambda x: x.year, lambda x: x.month, lambda x: x.isocalendar()[1]]).apply(cumulate_returns)
    elif convert_to == 'quarterly':
        return returns.groupby(
            [lambda x: x.year, lambda x: int(math.ceil(x.month / 3.))]).apply(cumulate_returns)
    elif convert_to == 'monthly':
        return returns.groupby(
            [lambda x: x.year, lambda x: x.month]).apply(cumulate_returns)
    elif convert_to == 'yearly':
        return returns.groupby(
            [lambda x: x.year]).apply(cumulate_returns)
    else:
        ValueError('convert_to must be weekly, monthly, quarterly or yearly')


def annualized_yield(prices, period):
    """
    计算区间收益率的年化收益率

    :param prices: 资产日行情数据， index为日期
    :param period: 区间长度，以自然日为标准  # todo 最好可以自动统计，用户很难知道多少天，下面函数类同
    :return: 与quote相同索引的年化收益率序列
    """
    temp = prices.reindex(pd.date_range(prices.index.min(), prices.index.max())).ffill().pct_change(freq=f'{period}D')
    yld = (temp + 1) ** (365 / period) - 1  # todo 不同类型的资产年化收益率计算方式不同，下面函数类同
    out = yld.reindex(prices.index)
    return out


def annual_return(common_data, data_type='prices', freq='daily', days_365=False):
    """
    计算年化收益率

    :param pd.Series common_data: index为带杠日期字符串，columns为一个净值序列或收益率序列
    :param str data_type: calc_array的数据类型，支持'prices'-净值序列，'returns'-收益率序列
    :param str freq: 输入的净值频率，可选daily,weekly,monthly,quarterly,yearly
    :param bool days_365: 用365天数年化还是用252天数年化
    :return: float
    """
    period_ret = period_return(common_data, data_type)
    if period_ret is None:
        LOG.error('输入的序列长度不够，无法计算区间收益')
        return 0

    if freq == 'daily' and days_365:
        try:
            numerator = time_tool.calc_date_diff(common_data.index[0], common_data.index[-1], 'days', '%Y-%m-%d')
        except:
            numerator = len(common_data)
        denominator = 365
    else:
        numerator = len(common_data)
        denominator = annualization_factor(freq, days_365=days_365)
    if data_type == 'prices':
        numerator = numerator - 1

    if numerator == 0:
        LOG.error('输入的序列长度不够，无法计算区间收益')
        return 0

    num_years = numerator / denominator

    return (period_ret + 1) ** (1 / num_years) - 1


def annualized_rolling_mean_yield(prices, period):
    """
    计算区间内平均日收益率的年化收益率

    :param prices: 资产日行情数据， index为日期
    :param period: 区间长度，计算平均收益率的区间长度
    :return: 与quote相同索引的年化收益率序列
    """
    temp = prices.reindex(pd.date_range(prices.index.min(), prices.index.max())).ffill().pct_change()
    rolling_mean = temp.rolling(period).mean()
    yld = (rolling_mean + 1) ** (365) - 1
    out = yld.reindex(prices.index)
    return out


def annualized_expanding_mean_yield(prices):
    """
    计算资产日收益率的年化收益率

    :param prices: 资产日行情数据， index为日期
    :return: 与quote相同索引的年化收益率序列
    """
    prices.index = pd.to_datetime(prices.index)
    temp = prices.reindex(pd.date_range(prices.index.min(), prices.index.max())).ffill().bfill()
    temp = temp / temp.iloc[0] - 1
    temp_yield = (temp + 1) ** (365 / temp.expanding().count()) - 1
    return temp_yield


def cum_returns(returns, return_type=0):
    """
    将指数或者基金的收益率序列转换成累计收益率序列

    :param pd.DataFrame or pd.Series returns: index为time，columns为一个或多个收益率序列
    :param int return_type: 返回类型，0-累计收益率，1-累计净值
    :return: array-like
    """
    if returns.shape[0] < 1:
        LOG.error('输入的收益率序列为空，请重新输入')
        return

    result = returns.fillna(0)
    result = (result + 1).cumprod()

    if return_type == 0:
        result = result - 1
    else:
        result = result * return_type

    return result


def period_return(common_data, data_type='prices'):
    """
    计算区间收益

    :param pd.Series common_data: index为带杠日期字符串，columns为一个净值序列或收益率序列
    :param str data_type: calc_array的数据类型，支持'prices'-净值序列，'returns'-收益率序列
    :return: pd.Series
    """
    if data_type == 'prices':
        if common_data.shape[0] <= 1:
            LOG.error("输入的价格序列不足两行，无法转换成收益率序列，请重新输入")
            return

        result = common_data.ffill().bfill()
        result = result.iloc[-1] / result.iloc[0] - 1
    elif data_type == 'returns':
        result = cum_returns(common_data, return_type=1).iloc[-1] - 1
    else:
        LOG.error('data_type只支持prices或者returns，请重新输入')
        return

    return result


def rolling_holding_return(common_data, rolling_window):
    """
    todo
    自起按日计算产品持有一定期限（一年，两年）的收益率情况，统计不同市场行情下的最大收益，最小收益，中位数，期望，正收益概率等
    最能反映投资者在不同产品持有体验上的差异

    :return:
    """
    pass


def capture_ret(bench_returns, target_returns=None, up_or_down='up'):
    """
    计算几何平均上行捕获收益率或下行捕获收益率

    :param pd.Series bench_returns: 基准收益率序列，如果计算指数，则target_ret为空
    :param pd.Series target_returns: 需要计算的基金、基金经理等收益率序列，index需要与bench_ret一致
    :param up_or_down: 'up'-上行，'down'-下行
    :return: float
    """
    if target_returns is None and up_or_down == 'up':
        temp = bench_returns.where(bench_returns > 0) + 1
    elif target_returns is None and up_or_down == 'down':
        temp = bench_returns.where(bench_returns < 0) + 1
    elif target_returns is not None and up_or_down == 'up':
        temp = target_returns.where(bench_returns > 0) + 1
    elif target_returns is not None and up_or_down == 'down':
        temp = target_returns.where(bench_returns < 0) + 1
    else:
        LOG.error('请按要求输入参数')
        return

    log_mean = np.log(temp).sum() / temp.count()
    cap_ret = np.exp(log_mean) - 1

    return cap_ret


def capture_ratio(bench_returns, target_returns, up_or_down='up'):
    """
    计算几何平均上行捕获率或下行捕获率

    :param pd.Series bench_returns: 基准收益率序列，如果计算指数，则target_ret为空
    :param pd.Series target_returns: 需要计算的基金、基金经理等收益率序列，index需要与bench_ret一致
    :param up_or_down: 'up'-上行，'down'-下行
    :return: float
    """
    target_cap_ret = capture_ret(bench_returns, target_returns=target_returns, up_or_down=up_or_down)
    bench_cap_ret = capture_ret(bench_returns, up_or_down=up_or_down)
    cap_ratio = target_cap_ret / bench_cap_ret
    return cap_ratio


def annual_volatility(returns, freq='daily', days_365=False):
    """
    计算年化波动率

    :param pd.Series returns: 收益率序列
    :param str freq: 输入的收益率频率，可选daily,weekly,monthly,quarterly,yearly
    :return: float
    """
    if len(returns) < 2:
        LOG.error('输入的收益率序列小于2个值，无法计算标准差')
        return

    period_std = returns.std()
    if (freq == 'daily') and days_365:
        ann_factor = 365
    else:
        ann_factor = annualization_factor(freq, days_365=days_365)
    annual_std = period_std * np.sqrt(ann_factor)

    return annual_std


def downside_risk(returns, required_return=0, freq='daily'):
    """
    计算下行风险

    :param pd.Series returns: 收益率序列
    :param float required_return: 需要达到的收益
    :param freq: 输入的收益率频率，可选daily,weekly,monthly,quarterly,yearly
    :return: float
    """
    if len(returns) < 2:
        LOG.error('输入的收益率序列小于2个值，无法计算标准差')
        return

    tmp_ret = returns - required_return
    tmp_ret[tmp_ret > 0] = 0
    result = np.sqrt((tmp_ret ** 2).sum() / (tmp_ret.count() - 1))
    ann_factor = annualization_factor(freq)
    result = result * np.sqrt(ann_factor)

    return result


def skewness(returns):
    """
    计算偏度

    :param pd.Series returns: 收益率序列
    :return: float
    """
    if returns.dropna().shape[0] < 3:
        LOG.error('输入的收益率序列中不为空的数小于3个，无法计算偏度，请重新输入')
        return

    return returns.skew()


def kurtosis(returns):
    """
    计算峰度

    :param pd.Series returns: 收益率序列
    :return: float
    """
    if returns.dropna().shape[0] < 4:
        LOG.error('输入的收益率序列中不为空的数小于4个，无法计算峰度，请重新输入')
        return

    return returns.kurt()


def beta(returns, bench_returns):
    """
    回归计算的beta

    :param returns:
    :param bench_returns:
    :return:
    """
    X = sm.add_constant(bench_returns)
    model_result = sm.OLS(X, returns, missing='drop').fit()
    beta = model_result.params[1].iloc[0]
    return beta


def alpha(returns, bench_returns):
    """
    回归计算的alpha

    :param returns:
    :param bench_returns:
    :return:
    """
    X = sm.add_constant(bench_returns)
    model_result = sm.OLS(X, returns, missing='drop').fit()
    alpha = model_result.params[0].iloc[0]
    return alpha


def beta_v2(returns, bench_returns):
    """
    目前数据库里计算衍生指标采用的算法

    :param returns:
    :param bench_returns:
    :return:
    """
    corr = returns.corr(bench_returns)
    std = returns.std()
    b_std = bench_returns.std()
    beta = corr * std / b_std

    return beta


def alpha_v2(returns, bench_returns, risk_free_ret=0):
    """
    目前数据库里计算衍生指标采用的算法，年化詹森阿尔法

    :param returns:
    :param bench_returns:
    :param risk_free_ret
    :return:
    """
    beta = beta_v2(returns, bench_returns)
    annual_ret = annual_return(returns, 'returns')
    annual_ret_b = annual_return(bench_returns, 'returns')
    alpha = annual_ret - risk_free_ret - beta * (annual_ret_b - risk_free_ret)

    return alpha


def track_error(returns, bench_returns, freq='daily'):
    """

    :param returns:
    :param bench_returns:
    :param freq:
    :return:
    """
    active_return = returns - bench_returns
    tra_error = annual_volatility(active_return, freq=freq)
    return tra_error


def max_drawdown(common_data, return_max_only=True, data_type='prices'):
    """
    计算最大回撤

    :param pd.Series common_data: index为带杠日期字符串，columns为一个净值序列或收益率序列
    :param bool return_max_only: 是否只返回最大回撤，默认True，若没有回撤，返回值为0。
                                 若为False，则返回drawdown, max drawdown, max drawdown duration
    :param str data_type: calc_array的数据类型，支持'prices'-净值序列，'returns'-收益率序列

    :return:
    """

    if data_type == 'prices':
        prices = common_data.dropna()
        prices = list(prices.values)
    elif data_type == 'returns':
        prices_temp = (common_data + 1).cumprod()
        prices_temp = list(prices_temp.values)
        prices = [1]
        prices.extend(prices_temp)
    prices = pd.Series(prices)

    if prices.shape[0] == 0:
        LOG.error('输入的价格序列全为空，请检查输入参数')
        return None

    if return_max_only:
        result = (1 - prices / prices.cummax()).max()
        return result
    else:
        idx = prices.index
        hwm = np.zeros(len(idx))

        # Create the high water mark
        for t in range(1, len(idx)):
            hwm[t] = max(hwm[t - 1], prices.iloc[t])

        # Calculate the drawdown and duration statistics
        perf = pd.DataFrame(index=idx)
        perf["Drawdown"] = (hwm - prices) / hwm
        perf["Drawdown"].iloc[0] = 0.0
        perf["DurationCheck"] = np.where(perf["Drawdown"] == 0, 0, 1)
        duration = max(sum(1 for i in g if i == 1) for k, g in groupby(perf["DurationCheck"]))

        return perf["Drawdown"], np.max(perf["Drawdown"]), duration


def max_drawdown_series(prices):
    """
    计算每时每刻的最大回撤数据

    :param pd.Series prices: 价格序列
    :return:
    """

    idx = prices.index
    hwm = pd.Series(0, index=idx)

    # Create the high water mark
    for i, t in enumerate(idx[1:]):
        hwm[t] = max(hwm[idx[i]], prices[t])
        # if t != '2020-03-12':
        #     hwm[t] = max(hwm[idx[i]], prices[t])
        # elif t == '2020-03-12':  # 以该时间节点分割，前后的最大回撤分别计算
        #     hwm[t] = prices[t]
        # else:
        #     raise NotImplementedError
    hwm[idx[0]] = prices[idx[0]]
    max_drawdown_series = (prices - hwm) / hwm

    return max_drawdown_series


def max_drawdown_period(prices):
    """

    :param pd.Series prices: 价格序列
    :return:
    """
    tmp = prices.dropna().shape[0]
    if tmp == 0:
        LOG.error('输入的价格序列全为空，请检查输入参数')
        return
    price_cummax = prices.cummax()
    drawdown = 1 - prices / price_cummax
    price_max_date = price_cummax.idxmax()
    drawdown_max_date = drawdown.idxmax()
    maxdrawdown_length = pd.date_range(price_max_date, drawdown_max_date).shape[0]
    return price_max_date, drawdown_max_date, maxdrawdown_length


def max_continuous_days(prices, type='profit'):
    """
    求最大连续盈利（亏损）天数
    :param prices:
    :param type: profit：最大连续盈利天数；loss：最大连续亏损天数
    :return:
    """
    daily_return = prices.pct_change().reset_index()
    daily_return.columns = ['trade_date', 'daily_return']
    if type == 'profit':
        opposite_ret_days = daily_return.loc[daily_return['daily_return'] < 0]
    elif type == 'loss':
        opposite_ret_days = daily_return.loc[daily_return['daily_return'] > 0]
    max_con_days = pd.Series(opposite_ret_days.index).diff() - 1
    return max_con_days.max()


def max_continuous_profit_or_loss(prices, initial_value, type='profit', method=2):
    """
    最大日连续盈利或亏损额：
    注：暂时将两个函数合并为一个
    两种理解：
    1. 最大连续盈利区间内的盈利或亏损额
    2. 最大连续盈利或坤孙额
    :param prices: 策略净值序列
    :return:
    """

    if method == 1:
        daily_return = prices.pct_change().reset_index()
        daily_return.columns = ['trade_date', 'daily_return']
        if type == 'profit':
            opposite_ret_days = daily_return.loc[daily_return['daily_return'] < 0]
        else:
            opposite_ret_days = daily_return.loc[daily_return['daily_return'] > 0]
        opposite_ret_days['max_con_profit_or_loss_days'] = (pd.Series(opposite_ret_days.index).diff() - 1).values
        next_opposite_begin_index = opposite_ret_days['max_con_profit_days'].idxmax()
        last_opposite_end_index = opposite_ret_days.index[
            opposite_ret_days.index.get_loc(next_opposite_begin_index) - 1]
        # period_begin_index = last_negative_end_index+1
        period_end_index = next_opposite_begin_index - 1
        max_profit_or_loss = prices.iloc[period_end_index] / prices.iloc[last_opposite_end_index] * initial_value
    else:
        daily_return = prices.pct_change()
        if type == 'profit':
            positive_daily_return = daily_return.where(daily_return > 0)
        else:
            positive_daily_return = daily_return.where(daily_return < 0)

        positive_cum_ret = (positive_daily_return.fillna(0) + 1).cumprod()
        positive_periods_end_cum_ret = positive_cum_ret[positive_cum_ret.duplicated()]
        max_profit_or_loss = positive_periods_end_cum_ret.pct_change().abs().max() * initial_value
    return max_profit_or_loss


# def max_continuous_loss(prices,initial_value, method=2):
#     """
#     最大日连续亏损额：
#     两种理解：
#     1. 最大连续盈利区间内的亏损额
#     2. 最大连续亏损额
#     :param prices: 策略净值序列
#     :return:
#     """
#
#     if method==1:
#         daily_return = prices.pct_change().reset_index()
#         daily_return.columns = ['trade_date', 'daily_return']
#         positive_ret_days = daily_return.loc[daily_return['daily_return'] > 0]
#         positive_ret_days['max_con_profit_days'] = (pd.Series(positive_ret_days.index).diff() - 1).values
#         next_positive_begin_index = positive_ret_days['max_con_profit_days'].idxmax()
#         last_positive_end_index = positive_ret_days.index[positive_ret_days.index.get_loc(next_positive_begin_index)-1]
#         # period_begin_index = last_negative_end_index+1
#         period_end_index = next_positive_begin_index-1
#         max_loss = prices.iloc[period_end_index] / prices.iloc[last_positive_end_index] * initial_value
#     else:
#         daily_return = prices.pct_change()
#         negative_daily_return = daily_return.where(daily_return < 0)
#         negative_cum_ret = (negative_daily_return.fillna(0)+1).cumprod()
#         negative_periods_end_cum_ret = negative_cum_ret[negative_cum_ret.duplicated()]
#         max_loss = negative_periods_end_cum_ret.pct_change().min() * initial_value
#     return - max_loss

def strategy_duration(prices):
    """

    :param prices: 策略净值序列，以时间为index
    :return:
    """
    duration = pd.date_range(prices.index.min(), prices.index.max()).shape[0]
    return duration


def turnover(trade_info, initial_value):
    """
    换手率是策略期间发生交易的总金额与初始资金的比率
    :param pd.DataFrame trade_info: 必须包含策略期间每笔交易（包括买入和卖出）的金额，
    例如：
    trade_id  value
    aaaaa    23525.66
    bbbbb    34555.66
    ccccc    59368.66
    ddddd    25487.66
    :param float initial_value: 策略初始资金
    :return:
    """
    turnover_ratio = trade_info['value'].sum() / 2 / initial_value
    return turnover_ratio


def turnover_days(trade_info, prices, initial_value):
    """
    换手率是策略期间发生交易的总金额与初始资金的比率
    :param prices: 策略净值
    :param pd.DataFrame trade_info: 必须包含策略期间每笔交易（包括买入和卖出）的金额，
    例如：
    trade_id  value
    aaaaa    23525.66
    bbbbb    34555.66
    ccccc    59368.66
    ddddd    25487.66
    :param float initial_value: 策略初始资金
    :return:
    """
    _turnover = turnover(trade_info, initial_value)
    _duration = strategy_duration(prices)
    turnover_day = _duration / _turnover
    return turnover_day


def general_order_analysis(trade_info, ):
    """
    交易统计，开仓平仓算两次交易.
    :param trade_info: 必须包含策略期间每笔交易的具体信息，包括：
    交易时间：datetime
    交易标的：code
    交易数量：amount, 买入为正，卖出为负
    交易价格：price
    交易方向：direction
    trade_id  code    datetime  amount
    aaaaa    000001 2020-03-22   100
    bbbbb    000002 2020-03-22   100
    ccccc    000002 2020-03-23   -100
    ddddd    000004 2020-03-24   100
    :return:
    交易总数量：total_num
    未平仓交易总数量: unclosed_num
    以下暂未实现
    # 盈利交易次数: profit_num
    # 亏损交易次数: loss_num
    # 胜率：winning_rate
    # 平均盈利额: average_profit
    # 平均亏损额: average_loss
    # 平均盈利/平均亏损: avg_profit_loss_ratio
    # 资金周转周期: turnover_days
    """
    total_num = trade_info.shape[0]
    unclosed_code = trade_info.groupby('code')['amount'].sum()
    unclosed_num = (unclosed_code > 0).sum()
    result = dict()
    result['total_num'] = total_num
    result['unclose_num'] = unclosed_num
    return result


def sharpe_ratio(returns, risk_free_ret=0.02, freq='daily', days_365=False):
    """
    计算夏普比率

    :param pd.Series returns: 收益率序列
    :param float risk_free_ret: 年化无风险利率
    :param str freq: 输入的收益率频率，可选daily,weekly,monthly,quarterly,yearly
    :return: float
    """

    ann_ret = annual_return(returns, 'returns', freq, days_365=days_365)
    ann_std = annual_volatility(returns, freq, days_365=days_365)

    if ann_std is None:
        LOG.error('输入的收益率序列的标准差无法计算为None，无法计算夏普比率')
        return 0
    if ann_std < 0.00001:
        LOG.error('输入的收益率序列的值完全相同，标准差为0或几乎为0，无法计算夏普比率')
        return 0

    sharpe = (ann_ret - risk_free_ret) / ann_std

    return sharpe


def info_ratio(returns, bench_returns, freq='daily'):
    """
    计算信息比率

    :param pd.Series returns: 收益率序列
    :param pd.Series bench_returns: 年化无风险利率
    :param str freq: 输入的收益率频率，可选daily,weekly,monthly,quarterly,yearly
    :return: float
    """
    ann_ret = annual_return(returns, 'returns', freq, days_365=False)
    bench_ann_ret = annual_return(bench_returns, 'returns', freq, days_365=False)
    ann_active_std = track_error(returns, bench_returns, freq=freq)
    if ann_active_std == 0:
        LOG.error('输入的收益率序列的值完全相同，标准差为0，无法计算夏普比率')
        return
    if ann_active_std is None:
        return
    info = (ann_ret - bench_ann_ret) / ann_active_std
    return info


def sortino_ratio(returns, required_return=0, freq='daily', _downside_risk=None):
    """
    计算索提诺比率

    :param pd.Series returns: 收益率序列
    :param float required_return: 需要达到的收益
    :param str freq: 输入的收益率频率，可选daily,weekly,monthly,quarterly,yearly
    :param float _downside_risk: 下行风险，可以自己输入，如果为None，则函数内部自己计算
    :return: float
    """

    ann_ret = annual_return(returns, 'returns', freq, days_365=False)
    ann_downside_risk = (_downside_risk
                         if _downside_risk is not None else
                         downside_risk(returns, required_return, freq))
    if ann_downside_risk == 0:
        LOG.error('下行风险为0，无法计算索提诺比率')
        return

    sortino = (ann_ret - required_return) / ann_downside_risk

    return sortino


def calmar_ratio(returns, risk_free_ret=0, freq='daily'):
    """
    计算卡玛比率

    :param pd.Series returns: 收益率序列
    :param float risk_free_ret: 年化无风险利率
    :param str freq: 输入的收益率频率，可选daily,weekly,monthly,quarterly,yearly
    :return: float
    """

    ann_ret = annual_return(returns, 'returns', freq, days_365=False)
    max_dd = max_drawdown(cum_returns(returns, 1))
    if max_dd == 0:
        LOG.error('最大回撤为0，无法计算卡玛比率')
        return

    calmar = (ann_ret - risk_free_ret) / max_dd

    return calmar


def m_square(returns, bench_returns, risk_free_ret=0, freq='daily'):
    """

    :param returns: 收益率序列
    :param risk_free_ret: 年化无风险利率
    :param bench_returns: 基准收益率序列
    :param str freq: 输入的收益率频率，可选daily,weekly,monthly,quarterly,yearly
    :return:
    """
    ann_ret = annual_return(returns, 'returns', freq, days_365=False)
    ann_vol = annual_volatility(returns, freq)
    bench_vol = annual_volatility(bench_returns, freq)
    return (ann_ret - risk_free_ret) / ann_vol * bench_vol + risk_free_ret


def omega_ratio(returns, risk_free_ret=0, freq='daily'):
    """
    计算欧米茄比率

    :param pd.Series returns: 收益率序列
    :param float risk_free_ret: 年化无风险利率
    :param str freq: 输入的收益率频率，可选daily,weekly,monthly,quarterly,yearly
    :return: float
    """
    adj_rf_rate = (risk_free_ret + 1) ** (FREQ_DAYS_DICT[freq] / 365) - 1
    tmp = returns - adj_rf_rate
    numerator = tmp.where(tmp > 0).sum()
    denominator = tmp.where(tmp < 0).sum()

    if denominator == 0:
        LOG.error('收益率序列的值均比这段时间的无风险利率高，无法计算欧米茄比率')
        return

    omega = - numerator / denominator

    return omega


def kappa_ratio(returns, risk_free_ret=0, freq='daily'):
    """
    计算卡帕比率

    :param pd.Series returns: 收益率序列
    :param float risk_free_ret: 年化无风险利率
    :param str freq: 输入的收益率频率，可选daily,weekly,monthly,quarterly,yearly
    :return: float
    """
    adj_rf_rate = (risk_free_ret + 1) ** (FREQ_DAYS_DICT[freq] / 365) - 1
    lpm3 = (returns ** 3).where((adj_rf_rate - returns) > 0, 0).mean()
    if lpm3 == 0:
        LOG.error('年化下行三阶矩为0，无法计算卡帕比率')
        return
    ann_lpm3 = lpm3 * annualization_factor(freq)
    ann_ret = annual_return(returns, 'return', freq, days_365=False)

    kappa = (ann_ret - risk_free_ret) / np.cbrt(ann_lpm3)

    return kappa


def winning_rate(asset_data, **kwargs):
    """
    求交易策略的胜率；仅限在asset_data中signal为1表示满仓 0表示空仓的情况下使用
    同时求出策略的平均盈利、最大盈利、平均亏损、最大亏损、盈亏比

    :param pd.DataFrame asset_data: 需含有两列 signal和ret 且index交易日期（日期格式为str，或者datetime都可以）;
                                    #########  signal代表当天的仓位，也即调仓发生在上一交易日收盘后  #########
                    |  ret   | signal
        |2010-01-03 |  0.01  |  1
        |2010-01-03 | 0.002  |  0
    :param kwargs:
        - pd.DataFrame, best_buy_period: 默认为None
                |  start       |     end
            |0 |  2018-12-05  |  2019-03-30
            |1 | 2020-03-12  |  2020-04-01
        - pd.DataFrame, best_sell_period: 默认为None
                |  start       |     end
            |0 |  2017-12-07  |  2018-01-15
            |1 | 2019-06-21  |  2019-08-25

    :return:
    """

    all_date = asset_data.index
    # 找到调仓日期, 注意这里因为signal是已经shift过一次的，因此这里的调仓日期其实是真实调仓日期的后一天；
    # 注意这里是包含了第一天的
    trade_date = asset_data['signal'][asset_data['signal'] != asset_data['signal'].shift(1)].dropna().index.tolist()
    # 总回测天数
    asset_data.index = pd.to_datetime(asset_data.index)
    total_days = (asset_data.index.max() - asset_data.index.min()).days + 1

    best_buy_period = kwargs.pop('best_buy_period', None)
    if best_buy_period is not None:
        best_buy_period = best_buy_period[
            (best_buy_period['end'] >= all_date[0]) & (best_buy_period['end'] <= all_date[-1])]
        best_buy_period_num = best_buy_period.shape[0]
    else:
        best_buy_period_num = 0
    best_sell_period = kwargs.pop('best_sell_period', None)
    if best_sell_period is not None:
        best_sell_period = best_sell_period.loc[
            (best_sell_period['end'] >= all_date[0]) & (best_sell_period['end'] <= all_date[-1])]
        best_sell_period_num = best_sell_period.shape[0]
    else:
        best_sell_period_num = 0

    # 求出每两个调仓日期之间 累计收益率
    cum_ret_dict = {}
    maxdrawdown_dict = {}
    in_best_buy_dict = {}
    in_best_sell_dict = {}
    signal_data_dict = {}
    for i in range(len(trade_date) - 1):
        # 最后一次交易未完结，所以不参与统计
        date = trade_date[i]
        next_date = trade_date[i + 1]
        temp_ret_series = asset_data['ret'][date:next_date]
        signal_data_dict[date] = asset_data['signal'][date]  # date前一天信号值
        cum_ret_series = (temp_ret_series[:-1] + 1).cumprod() - 1
        cum_ret_dict[date] = cum_ret_series[-1]  # 调仓期间的累计收益率
        if asset_data['signal'][date] == 1:
            maxdrawdown_dict[date] = max_drawdown(temp_ret_series[:-1], data_type='returns')
            if best_buy_period is not None:
                for ii in best_buy_period.index:
                    if (date >= best_buy_period.loc[ii, 'start']) and (date <= best_buy_period.loc[ii, 'end']):
                        in_best_buy_dict[date] = 1
        elif asset_data['signal'][date] == 0:
            maxdrawdown_dict[date] = max_drawdown(-temp_ret_series[:-1], data_type='returns')
            if best_sell_period is not None:
                for ii in best_sell_period.index:
                    if (date >= best_sell_period.loc[ii, 'start']) and (date <= best_sell_period.loc[ii, 'end']):
                        in_best_sell_dict[date] = 1

    signal_data = pd.Series(signal_data_dict)
    cum_ret_series = pd.Series(cum_ret_dict)
    maxdrawdown_series = pd.Series(maxdrawdown_dict)
    in_best_buy_series = pd.Series(in_best_buy_dict)
    in_best_sell_series = pd.Series(in_best_sell_dict)

    df = pd.concat([signal_data, cum_ret_series, maxdrawdown_series, in_best_buy_series, in_best_sell_series], axis=1)
    df.columns = ['signal', 'cum_ret', 'maxdrawdown', 'in_best_buy', 'in_best_sell']

    toal_cum_series = df['cum_ret']
    buy_cum_series = df['cum_ret'][df['signal'] == 1]  # 买入交易对应的收益
    sell_cum_series = df['cum_ret'][df['signal'] == 0]  # 卖出交易对应的收益
    buy_maxdrawdown_series = df['maxdrawdown'][df['signal'] == 1]  # 买入交易对应的期间最大回撤
    sell_maxdrawdown_series = df['maxdrawdown'][df['signal'] == 0]  # 卖出交易对应的期间最大回撤
    buy_in_best_buy_series = df['in_best_buy'][df['signal'] == 1]  # 买入交易对应的期间最大回撤
    sell_in_best_sell_series = df['in_best_sell'][df['signal'] == 0]  # 卖出交易对应的期间最大回撤

    buy_win_times = (buy_cum_series[buy_cum_series > 0]).size
    buy_win_rate = buy_win_times / buy_cum_series.size if buy_cum_series.size else np.NAN
    buy_max_profit = buy_cum_series.max()
    buy_max_loss = buy_cum_series.min()
    buy_max_maxdrawdown = buy_maxdrawdown_series.max()
    buy_mean_profit = buy_cum_series.mean()
    best_buy_times = (buy_in_best_buy_series[buy_in_best_buy_series > 0]).size
    if best_buy_period_num:
        best_buy_in_all_best_buy_rate = best_buy_times / best_buy_period_num if best_buy_period_num else np.NAN
        best_buy_in_all_buy_rate = best_buy_times / buy_cum_series.size if buy_cum_series.size else np.NAN
    else:
        best_buy_in_all_best_buy_rate = np.NAN
        best_buy_in_all_buy_rate = np.NAN

    sell_win_times = (sell_cum_series[sell_cum_series < 0]).size
    sell_win_rate = sell_win_times / sell_cum_series.size if sell_cum_series.size else np.NAN
    sell_max_profit = - sell_cum_series.min()
    sell_max_loss = - sell_cum_series.max()
    sell_max_maxdrawdown = sell_maxdrawdown_series.max()
    sell_mean_profit = - sell_cum_series.mean()
    best_sell_times = (sell_in_best_sell_series[sell_in_best_sell_series > 0]).size
    if best_sell_period_num:
        best_sell_in_all_best_sell_rate = best_sell_times / best_sell_period_num if best_sell_period_num else np.NAN
        best_sell_in_all_sell_rate = best_sell_times / sell_cum_series.size if sell_cum_series.size else np.NAN
    else:
        best_sell_in_all_best_sell_rate = np.NAN
        best_sell_in_all_sell_rate = np.NAN

    trade_times = toal_cum_series.shape[0]  # 总交易次数
    avg_days_per_trade = int(total_days / (trade_times + 1))
    total_win_rate = (buy_win_times + sell_win_times) / trade_times if trade_times else np.NAN
    if (best_buy_period_num + best_sell_period_num):
        best_buy_sell_in_all_best_buy_sell_rate = (best_buy_times + best_sell_times) / (best_buy_period_num + best_sell_period_num)
        best_buy_sell_in_all_rate = (best_buy_times + best_sell_times) / trade_times if trade_times else np.NAN
    else:
        best_buy_sell_in_all_best_buy_sell_rate = np.NAN
        best_buy_sell_in_all_rate = np.NAN

    # 盈亏比
    win_rate_analysis = pd.Series([f"{int(trade_times)}",
                                   f"{int(avg_days_per_trade)}",
                                   f"{float(total_win_rate) * 100:.2f}%",
                                   f"{float(buy_win_rate) * 100:.2f}%",
                                   f"{float(buy_max_profit) * 100:.2f}%",
                                   f"{float(buy_max_loss) * 100:.2f}%",
                                   f"{float(buy_max_maxdrawdown) * 100:.2f}%",
                                   f"{float(buy_mean_profit) * 100:.2f}%",
                                   f"{float(sell_win_rate) * 100:.2f}%",
                                   f"{float(sell_max_profit) * 100:.2f}%",
                                   f"{float(sell_max_loss) * 100:.2f}%",
                                   f"{float(sell_max_maxdrawdown) * 100:.2f}%",
                                   f"{float(sell_mean_profit) * 100:.2f}%",
                                   f"{float(best_buy_in_all_best_buy_rate) * 100:.2f}%",
                                   f"{float(best_buy_in_all_buy_rate) * 100:.2f}%",
                                   f"{float(best_sell_in_all_best_sell_rate) * 100:.2f}%",
                                   f"{float(best_sell_in_all_sell_rate) * 100:.2f}%",
                                   f"{float(best_buy_sell_in_all_best_buy_sell_rate) * 100:.2f}%",
                                   f"{float(best_buy_sell_in_all_rate) * 100:.2f}%"],

                                  index=['交易次数', '平均多少天交易一次', '总胜率',
                                         '买入信号胜率', '买入最大盈利', '买入最大亏损', '买入最大回撤', '买入平均盈利',
                                         '卖出信号胜率', '卖出最大盈利', '卖出最大亏损', '卖出最大回撤', '卖出平均盈利',
                                         '最佳买入占所有最佳买点比例', '最佳买入占所有买入信号比例',
                                         '最佳卖出占所有最佳卖点比例', '最佳卖出占所有卖出信号比例',
                                         '最佳买卖占所有最佳买卖点比例', '最佳买卖占所有买卖信号比例',
                                         ])
    return win_rate_analysis


def simple_perf(returns, freq='daily', days_365=False):
    sharpe_r = sharpe_ratio(returns, freq=freq, days_365=days_365)
    volatility = annual_volatility(returns, freq=freq, days_365=days_365)
    equity = (1 + returns).cumprod()
    drawdown = max_drawdown(returns, data_type='returns')
    annual_ret = annual_return(equity, freq=freq, days_365=days_365)
    calmar_r = annual_ret / drawdown if drawdown else 0
    temp_returns = returns.copy()
    temp_returns.index = pd.to_datetime(temp_returns.index).date
    month_ret_series = aggregate_returns(temp_returns, convert_to='monthly')
    week_ret_series = aggregate_returns(temp_returns, convert_to='weekly')
    monthly_ret = month_ret_series.mean()
    weekly_ret = week_ret_series.mean()
    perf = pd.Series([f"{float(annual_ret) * 100:.2f}%",
                      f"{float(volatility) * 100:.2f}%",
                      f"{float(sharpe_r) :.2f}",
                      f"{float(drawdown) * 100:.2f}%",
                      f"{float(calmar_r) :.2f}",
                      f"{float(monthly_ret) * 100:.2f}%",
                      f"{float(weekly_ret) * 100:.2f}%"
                      ],
                     index=['年化收益率', '年化波动率', '夏普比率', '最大回撤', '卡玛比率', '月收益率', '周收益率'])
    return perf


def trade_count(signal):
    if signal.isna().all():
        return 0
    else:
        temp = signal.fillna(0)
        trades = (temp != temp.shift()).sum()
        return trades


def active_ret_for_alpha_analysis(asset_type, asset_code, start_date, end_date):
    """
    alpha分析中的相对基准alpha，使用日收益率计算

    :param str asset_type: 计算类型，'fund'-基金，'manager'-基金经理
    :param str asset_code: 计算代码
    :param str start_date: 开始时间，格式"2020-08-01"
    :param str end_date: 结束时间，格式"2020-11-01"
    :return: pd.Series
    """
    if asset_type == 'fund':
        target_ret = fund_nav_related.get_fund_return(
            fund_code=asset_code,
            start_date=start_date,
            end_date=end_date
        )
        target_ret = target_ret.rename(columns={"end_date": 'tj'})
        index_code = fund_info.get_fund_benchmark(asset_code)
        if index_code.empty:
            LOG.error(f"没有找到{asset_code}这支基金的基准指数，请进行检查")
            return
        index_code = index_code['benchmark'].iloc[0]
    elif asset_type == 'manager':
        target_ret = fund_nav_related.get_manager_return(
            manager_code=asset_code,
            start_date=start_date,
            end_date=end_date,
            fund_type='00'
        )
        target_ret = target_ret.rename(columns={'end_date': 'tj'})
        index_code = 'TK00ZH'
    else:
        LOG.error(f"target_type只支持fund和manager，请重新输入")
        return
    target_ret = target_ret[['tj', 'daily_return']].rename(columns={'daily_return': 'target_ret'})
    index_ret = index_price_related.get_index_return(
        index_code=index_code,
        start_date=start_date,
        end_date=end_date
    ).rename(columns={'end_date': 'tj'})
    index_ret = index_ret[['tj', 'daily_return']].rename(columns={'daily_return': 'index_ret'})
    merge_ret = target_ret.merge(index_ret, how='inner', on='tj')
    if merge_ret.empty:
        LOG.error(f"没有找到{asset_code}和{index_code}在{start_date}和{end_date}之间共同日期的日收益率")
        return
    merge_ret['active_ret'] = merge_ret['target_ret'] - merge_ret['index_ret']
    result = pd.Series(merge_ret['active_ret'].values, index=merge_ret['tj'])

    return result


def jensen_alpha_for_alpha_analysis(asset_type, asset_code, start_date, end_date):
    """
    alpha分析中的詹森alpha，使用日收益率计算

    :param str asset_type: 计算类型，'fund'-基金，'manager'-基金经理
    :param  str asset_code: 计算代码
    :param str start_date: 开始时间，格式"2020-08-01"
    :param str end_date: 结束时间，格式"2020-11-01"
    :return: pd.Series
    """
    if asset_type == 'fund':
        target_ret = fund_nav_related.get_fund_return(
            fund_code=asset_code,
            start_date=start_date,
            end_date=end_date
        )
        target_ret = target_ret.rename(columns={"end_date": 'tj'})
        index_code = fund_info.get_fund_benchmark(asset_code)
        if index_code.empty:
            LOG.error(f"没有找到{asset_code}这支基金的基准指数，请进行检查")
            return
        index_code = index_code['benchmark'].iloc[0]
        beta = fund_nav_related.get_performance_indicator_data(
            indicator='beta',
            asset_type='fund',
            freq='D',
            asset_code=asset_code,
            period_interval=[3],
            start_date=start_date,
            end_date=end_date,
            fund_type=None
        )
        beta = beta[['end_date', 'beta']].rename(columns={'end_date': 'tj'})
    elif asset_type == 'manager':
        target_ret = fund_nav_related.get_manager_return(
            manager_code=asset_code,
            start_date=start_date,
            end_date=end_date,
            fund_type='00'
        )
        target_ret = target_ret.rename(columns={'end_date': 'tj'})

        index_code = 'TK00ZH'
        beta = fund_nav_related.get_performance_indicator_data(
            indicator='beta',
            asset_type='manager',
            freq='D',
            asset_code=asset_code,
            period_interval=[3],
            start_date=start_date,
            end_date=end_date,
        )
        beta = beta[['end_date', 'beta']].rename(columns={'end_date': 'tj'})
    else:
        LOG.error(f"target_type只支持fund和manager，请重新输入")
        return
    target_ret = target_ret[['tj', 'daily_return']].rename(columns={'daily_return': 'target_ret'})

    index_ret = index_price_related.get_index_return(
        index_code=index_code,
        start_date=start_date,
        end_date=end_date
    ).rename(columns={'end_date': 'tj'})
    index_ret = index_ret[['tj', 'daily_return']].rename(columns={'daily_return': 'index_ret'})
    merge_df = target_ret.merge(index_ret, how='inner', on='tj')
    merge_df = merge_df.merge(beta, how='inner', on='tj')
    if merge_df.empty:
        LOG.error(f"没有找到{asset_code}和{index_code}在{start_date}和{end_date}之间共同日期的日收益率、beta")
        return

    merge_df['jensen_alpha'] = (merge_df['target_ret'] - RF_RATE_DAY -
                                merge_df['beta'] * (merge_df['index_ret'] - RF_RATE_DAY))
    result = pd.Series(merge_df['jensen_alpha'].values, index=merge_df['tj'])

    return result


def stats_holding_ret(daily_return, stats_years):
    """
    输入一支基金或指数或……的日收益率序列，统计持有其一段时间的收益率情况。
    比如，输入一支基金从2015年1月5日到2020年1月5日的日收益率序列，统计持有其1年的收益率情况，
    会从2015年1月5日开始，滚动计算这天到一年后这段时间的累计收益率，然后统计累计收益率的最大值、最小值、中位数和正收益概率

    :param pd.Series daily_return: 日收益率序列
    :param int stats_years: 持有时间，1表示1年，2表示2年，以此类推
    :return: dict，返回累计收益率序列、累计收益率最大值、累计收益率最小值、累计收益率中位数、正收益概率
    """
    reindex_ret = calendar_reindex(daily_return)
    reindex_ret = reindex_ret.fillna(0)
    window = stats_years * 365
    cum_ret_series = reindex_ret.rolling(window).apply(lambda x: period_return(x, 'returns'), raw=False)
    cum_ret_series = cum_ret_series.dropna()
    if cum_ret_series.empty:
        LOG.error('输入的daily_return长度比stats_years短，请重新输入')
        return
    max_cum_ret = cum_ret_series.max()
    min_cum_ret = cum_ret_series.min()
    median_cum_ret = cum_ret_series.median()
    postive_cum_ret_prob = cum_ret_series[cum_ret_series > 0].shape[0] / cum_ret_series.shape[0]

    res_dict = {
        'cum_ret_series': cum_ret_series,
        'max_cum_ret': max_cum_ret,
        'min_cum_ret': min_cum_ret,
        'median_cum_ret': median_cum_ret,
        'postive_cum_ret_prob': postive_cum_ret_prob,
    }

    return res_dict


def stats_record_high_times(return_series):
    """
    输入一支基金或指数或……的收益率序列，统计其累计收益率创新高的次数

    :param pd.Series return_series: 收益率序列
    :return:
    """
    res_df = cum_returns(return_series)
    res_df = res_df.rename('ret').to_frame()
    res_df['cummax'] = res_df['ret'].cummax().shift()

    filter_df = res_df[res_df['ret'] > res_df['cummax']]

    return filter_df.shape[0]


class TearsheetStatistics:
    """
    Displays a Matplotlib-generated 'one-pager' as often
    found in institutional strategy performance reports.
    """

    def __init__(
            self,
            strategy_equity,
            benchmark_equity=None,
            title=None
    ):
        """
        画图需要的净值序列

        :param pd.Series strategy_equity: 策略的净值序列，index为datetime.date
        :param pd.Series benchmark_equity: 基准的净值序列，index为datetime.date
        :param str title: 图像的表头
        """
        self.strategy_equity = strategy_equity
        self.benchmark_equity = benchmark_equity
        self.title = title

    def get_results(self, equity_df):
        """
        Return a dict with all important results & stats.
        """
        # Returns
        equity_df["returns"] = equity_df["Equity"].pct_change().fillna(0.0)

        # Cummulative Returns
        equity_df["cum_returns"] = np.exp(np.log(1 + equity_df["returns"]).cumsum())

        # Drawdown, max drawdown, max drawdown duration
        dd_s, max_dd, dd_dur = max_drawdown(equity_df["cum_returns"], return_max_only=False)

        # Equity statistics
        statistics = {}
        statistics["sharpe"] = sharpe_ratio(equity_df["returns"])
        statistics["drawdowns"] = dd_s
        statistics["max_drawdown"] = max_dd
        statistics["max_drawdown_pct"] = max_dd
        statistics["max_drawdown_duration"] = dd_dur
        statistics["equity"] = equity_df["Equity"]
        statistics["returns"] = equity_df["returns"]
        statistics["cum_returns"] = equity_df["cum_returns"]
        return statistics

    def _plot_equity(self, strat_stats, bench_stats=None, ax=None, **kwargs):
        """
        Plots cumulative rolling returns versus some benchmark.
        """

        def format_two_dec(x, pos):
            return '%.2f' % x

        equity = strat_stats['cum_returns']

        if ax is None:
            ax = plt.gca()

        y_axis_formatter = FuncFormatter(format_two_dec)
        ax.yaxis.set_major_formatter(FuncFormatter(y_axis_formatter))
        ax.xaxis.set_tick_params(reset=True)
        ax.yaxis.grid(linestyle=':')
        ax.xaxis.set_major_locator(mdates.YearLocator(1))
        ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y'))
        ax.xaxis.grid(linestyle=':')

        equity.plot(lw=2, color='green', alpha=0.6, x_compat=False,
                    label='Strategy', ax=ax, **kwargs)
        if bench_stats is not None:
            bench_stats['cum_returns'].plot(
                lw=2, color='gray', alpha=0.6, x_compat=False,
                label='Benchmark', ax=ax, **kwargs
            )

        ax.axhline(1.0, linestyle='--', color='black', lw=1)
        ax.set_ylabel('Cumulative returns')
        ax.legend(loc='best')
        ax.set_xlabel('')
        plt.setp(ax.get_xticklabels(), visible=True, rotation=0, ha='center')
        return ax

    def _plot_drawdown(self, stats, bench_stats=None, ax=None, **kwargs):
        """
        Plots the underwater curve
        """

        def format_perc(x, pos):
            return '%.0f%%' % x

        drawdown = stats['drawdowns']

        if ax is None:
            ax = plt.gca()

        y_axis_formatter = FuncFormatter(format_perc)
        ax.yaxis.set_major_formatter(FuncFormatter(y_axis_formatter))
        ax.yaxis.grid(linestyle=':')
        ax.xaxis.set_tick_params(reset=True)
        ax.xaxis.set_major_locator(mdates.YearLocator(1))
        ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y'))
        ax.xaxis.grid(linestyle=':')

        underwater = -100 * drawdown
        underwater.plot(ax=ax, lw=2, kind='area', color='green', alpha=0.3, label='Strategy', **kwargs)
        if bench_stats is not None:
            bench_drawdown = bench_stats['drawdowns']
            bench_underwater = -100 * bench_drawdown
            y_min = pd.concat([underwater, bench_underwater]).min()
            bench_underwater.plot(ylim=(y_min - 5, 0), ax=ax, lw=2, kind='area', color='gray', alpha=0.3,
                                  label='Benchmark', **kwargs)
        ax.set_ylabel('')
        ax.set_xlabel('')
        ax.legend(loc='best')
        plt.setp(ax.get_xticklabels(), visible=True, rotation=0, ha='center')
        ax.set_title('Drawdown', fontweight='bold')
        return ax

    def _plot_monthly_returns(self, stats, ax=None, **kwargs):
        """
        Plots a heatmap of the monthly returns.
        """
        import seaborn as sns
        returns = stats['returns']
        if ax is None:
            ax = plt.gca()

        monthly_ret = aggregate_returns(returns, 'monthly')
        monthly_ret = monthly_ret.unstack()
        monthly_ret = np.round(monthly_ret, 3)
        monthly_ret.rename(
            columns={1: 'Jan', 2: 'Feb', 3: 'Mar', 4: 'Apr',
                     5: 'May', 6: 'Jun', 7: 'Jul', 8: 'Aug',
                     9: 'Sep', 10: 'Oct', 11: 'Nov', 12: 'Dec'},
            inplace=True
        )

        sns.heatmap(
            monthly_ret.fillna(0) * 100.0,
            annot=True,
            fmt="0.1f",
            annot_kws={"size": 8},
            alpha=1.0,
            center=0.0,
            cbar=False,
            cmap=cm.RdYlGn,
            ax=ax, **kwargs)
        ax.set_title('Monthly Returns (%)', fontweight='bold')
        ax.set_ylabel('')
        ax.set_yticklabels(ax.get_yticklabels(), rotation=0)
        ax.set_xlabel('')

        return ax

    def _plot_yearly_returns(self, stats, ax=None, **kwargs):
        """
        Plots a barplot of returns by year.
        """

        def format_perc(x, pos):
            return '%.0f%%' % x

        returns = stats['returns']

        if ax is None:
            ax = plt.gca()

        y_axis_formatter = FuncFormatter(format_perc)
        ax.yaxis.set_major_formatter(FuncFormatter(y_axis_formatter))
        ax.yaxis.grid(linestyle=':')

        yly_ret = aggregate_returns(returns, 'yearly') * 100.0
        yly_ret.plot(ax=ax, kind="bar")
        ax.set_title('Yearly Returns', fontweight='bold')
        ax.set_ylabel('')
        ax.set_xlabel('')
        ax.set_xticklabels(ax.get_xticklabels(), rotation=45)
        ax.xaxis.grid(False)

        return ax

    def _plot_txt_curve(self, stats, bench_stats=None, ax=None, **kwargs):
        """
        Outputs the statistics for the equity curve.
        """

        def format_perc(x, pos):
            return '%.0f%%' % x

        if ax is None:
            ax = plt.gca()

        y_axis_formatter = FuncFormatter(format_perc)
        ax.yaxis.set_major_formatter(FuncFormatter(y_axis_formatter))

        # Strategy statistics
        returns = stats["returns"]
        cum_returns = stats['cum_returns']
        tot_ret = cum_returns[-1] - 1.0
        annual_ret = annual_return(cum_returns)
        sharpe = sharpe_ratio(returns)
        sortino = sortino_ratio(returns)
        dd, dd_max, dd_dur = max_drawdown(cum_returns, return_max_only=False)

        # Benchmark statistics
        if bench_stats is not None:
            bench_returns = bench_stats["returns"]
            bench_cum_returns = bench_stats['cum_returns']
            bench_tot_ret = bench_cum_returns[-1] - 1.0
            bench_annual_ret = annual_return(bench_cum_returns)
            bench_sharpe = sharpe_ratio(bench_returns)
            bench_sortino = sortino_ratio(bench_returns)
            bench_dd, bench_dd_max, bench_dd_dur = max_drawdown(bench_cum_returns, return_max_only=False)

        # Strategy Values
        ax.text(7.50, 8.2, 'Strategy', fontweight='bold', horizontalalignment='right', fontsize=8, color='green')

        ax.text(0.25, 6.9, 'Total Return', fontsize=8)
        ax.text(7.50, 6.9, '{:.0%}'.format(tot_ret), fontweight='bold', horizontalalignment='right', fontsize=8)

        ax.text(0.25, 5.9, 'Annual Return', fontsize=8)
        ax.text(7.50, 5.9, '{:.2%}'.format(annual_ret), fontweight='bold', horizontalalignment='right', fontsize=8)

        ax.text(0.25, 4.9, 'Sharpe Ratio', fontsize=8)
        ax.text(7.50, 4.9, '{:.2f}'.format(sharpe), fontweight='bold', horizontalalignment='right', fontsize=8)

        ax.text(0.25, 3.9, 'Sortino Ratio', fontsize=8)
        ax.text(7.50, 3.9, '{:.2f}'.format(sortino), fontweight='bold', horizontalalignment='right', fontsize=8)

        ax.text(0.25, 2.9, 'Annual Volatility', fontsize=8)
        ax.text(7.50, 2.9, '{:.2%}'.format(returns.std() * np.sqrt(252)), fontweight='bold',
                horizontalalignment='right', fontsize=8)

        ax.text(0.25, 1.9, 'Max Drawdown', fontsize=8)
        ax.text(7.50, 1.9, '{:.2%}'.format(dd_max), color='red', fontweight='bold', horizontalalignment='right',
                fontsize=8)

        ax.text(0.25, 0.9, 'Max Drawdown Duration (Days)', fontsize=8)
        ax.text(7.50, 0.9, '{:.0f}'.format(dd_dur), fontweight='bold', horizontalalignment='right', fontsize=8)

        # Benchmark Values
        if bench_stats is not None:
            ax.text(10.0, 8.2, 'Benchmark', fontweight='bold', horizontalalignment='right', fontsize=8, color='gray')
            ax.text(10.0, 6.9, '{:.0%}'.format(bench_tot_ret), fontweight='bold', horizontalalignment='right',
                    fontsize=8)
            ax.text(10.0, 5.9, '{:.2%}'.format(bench_annual_ret), fontweight='bold', horizontalalignment='right',
                    fontsize=8)
            ax.text(10.0, 4.9, '{:.2f}'.format(bench_sharpe), fontweight='bold', horizontalalignment='right',
                    fontsize=8)
            ax.text(10.0, 3.9, '{:.2f}'.format(bench_sortino), fontweight='bold', horizontalalignment='right',
                    fontsize=8)
            ax.text(10.0, 2.9, '{:.2%}'.format(bench_returns.std() * np.sqrt(252)), fontweight='bold',
                    horizontalalignment='right', fontsize=8)
            ax.text(10.0, 1.9, '{:.2%}'.format(bench_dd_max), color='red', fontweight='bold',
                    horizontalalignment='right', fontsize=8)
            ax.text(10.0, 0.9, '{:.0f}'.format(bench_dd_dur), fontweight='bold', horizontalalignment='right',
                    fontsize=8)

        ax.set_title('Performance Indicators', fontweight='bold')

        ax.grid(False)
        ax.spines['top'].set_linewidth(2.0)
        ax.spines['bottom'].set_linewidth(2.0)
        ax.spines['right'].set_visible(False)
        ax.spines['left'].set_visible(False)
        ax.get_yaxis().set_visible(False)
        ax.get_xaxis().set_visible(False)
        ax.set_ylabel('')
        ax.set_xlabel('')

        ax.axis([0, 10, 0, 10])
        return ax

    def plot_results(self, filename=None):
        """
        Plot the Tearsheet
        """
        import seaborn as sns
        print('Plotting the tearsheet...')
        rc = {
            'lines.linewidth': 1.0,
            'axes.facecolor': '0.995',
            'figure.facecolor': '0.97',
            'font.family': 'serif',
            'font.serif': 'Ubuntu',
            'font.monospace': 'Ubuntu Mono',
            'font.size': 10,
            'axes.labelsize': 10,
            'axes.labelweight': 'bold',
            'axes.titlesize': 10,
            'xtick.labelsize': 8,
            'ytick.labelsize': 8,
            'legend.fontsize': 10,
            'figure.titlesize': 12
        }
        sns.set_context(rc)
        sns.set_style("whitegrid")
        sns.set_palette("deep", desat=.6)

        vertical_sections = 5
        fig = plt.figure(figsize=(16, 12))
        fig.suptitle(self.title, y=0.94, weight='bold')
        gs = gridspec.GridSpec(vertical_sections, 3, wspace=0.25, hspace=0.5)

        stats = self.get_results(self.strategy_equity)
        bench_stats = None
        if self.benchmark_equity is not None:
            bench_stats = self.get_results(self.benchmark_equity)

        ax_equity = plt.subplot(gs[:2, :])
        ax_drawdown = plt.subplot(gs[2, :])
        ax_monthly_returns = plt.subplot(gs[3, :2])
        ax_yearly_returns = plt.subplot(gs[3, 2])
        ax_txt_curve = plt.subplot(gs[4, 0])
        # ax_txt_trade = plt.subplot(gs[4, 1])
        # ax_txt_time = plt.subplot(gs[4, 2])

        self._plot_equity(stats, bench_stats=bench_stats, ax=ax_equity)
        self._plot_drawdown(stats, bench_stats=bench_stats, ax=ax_drawdown)
        self._plot_monthly_returns(stats, ax=ax_monthly_returns)
        self._plot_yearly_returns(stats, ax=ax_yearly_returns)
        self._plot_txt_curve(stats, bench_stats=bench_stats, ax=ax_txt_curve)
        # self._plot_txt_trade(stats, ax=ax_txt_trade)
        # self._plot_txt_time(stats, ax=ax_txt_time)

        # Plot the figure
        if filename is not None:
            tmp = os.path.join(BASE_DIR, f'{filename}_tearsheet.jpg')
            plt.savefig(tmp, dpi=300, bbox_inches='tight')
        plt.show()


if __name__ == '__main__':
    # signal_data = pd.DataFrame([['2018-01-01', 0.1, 1],
    #                             ['2018-01-02', 0.1, 0],
    #                             ['2018-01-03', -0.1, 0],
    #                             ['2018-01-04', -0.05, 0],
    #                             ['2018-01-05', 0.5, 0],
    #                             ['2018-01-06', 0.1, 0],
    #                             ['2018-01-07', 0.1, 1],
    #                             ['2018-01-08', -0.3, 0]],
    #                            columns=['tradedate', 'ret', 'signal'])
    # signal_data.set_index('tradedate', inplace=True)
    #
    # best_buy_period = pd.DataFrame([['2017-01-05', '2017-01-08'],
    #                                 # ['2018-01-05', '2018-01-08'],
    #                                 ['2019-01-05', '2019-01-08']],
    #                                columns=['start', 'end'])
    # winning_rate(signal_data, best_buy_period=best_buy_period)

    # 测试绩效指标
    prices_data = pd.DataFrame([['2018-01-01', 0.1],
                                ['2018-01-02', 0.8],
                                ['2018-01-03', 0.1],
                                ['2018-01-04', 0.05],
                                ['2018-01-05', 0.5],
                                ['2020-03-12', 0.1],
                                ['2020-03-13', 0.2],
                                ['2020-03-14', 0.3],
                                ['2020-03-15', 0.1]],
                               columns=['tradedate', 'price'])
    prices_data.set_index('tradedate', inplace=True)
    # 测试
    max_drawdown_series(prices_data['price'])

    # from quant_researcher.quant.project_tool.db_operator import db_conn
    # conn = db_conn.get_derivative_data_conn()
    # df = pd.read_sql(f"select day as date, daily_return "
    #                  f"from mf_di_idxdailyreturn "
    #                  f"where index_code = '000300' ", conn)
    # df['date'] = df['date'].astype(str)
    # ret_series = df.set_index('date')['daily_return']

    # # 测试sharpe_ratio
    # aaa = sharpe_ratio(ret_series, 0.015, 'daily')

    # 测试simple_perf
    # aaa = simple_perf(ret_series)

    # 测试stats_holding_ret
    df = fund_nav_related.get_fund_return('110022')
    df = df.set_index('end_date')['daily_return']
    bbb = stats_holding_ret(df, 2)

    # 测试stats_holding_ret
    ccc = stats_record_high_times(df)

    # 测试tearsheet
    strategy_equity = fund_nav_related.get_fund_nav(fund_code='110022', start_date='2015-01-01', end_date='2020-11-11')
    strategy_equity['end_date'] = pd.to_datetime(strategy_equity['end_date'])
    strategy_equity['end_date'] = strategy_equity['end_date'].dt.date
    strategy_equity = strategy_equity[['end_date', 'adjusted_nav']].rename(columns={'adjusted_nav': 'Equity'})
    strategy_equity = strategy_equity.set_index('end_date')

    benchmark_equity = index_price_related.get_index_close(
        index_code=['000300'],
        start_date='2015-01-01',
        end_date='2020-11-11'
    ).rename(columns={'end_date': 'trade_date'}).set_index(['trade_date', 'index_code'])['close'].unstack()
    benchmark_equity.index = pd.to_datetime(benchmark_equity.index)
    benchmark_equity.index = benchmark_equity.index.date
    benchmark_equity = benchmark_equity.rename(columns={'000300': 'Equity'})

    tearsheet = TearsheetStatistics(strategy_equity=strategy_equity, benchmark_equity=benchmark_equity, title='60/40 US Equities/Bonds')
    tearsheet.plot_results(filename='test')
